/* Copyright (C) 2008  Romolo Camplani, Marco Fortunato, Marco Marelli, Guido Rota, Fabio A. Schreiber et al.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package org.dei.perla.parser.util;

import java.io.File;
import java.util.HashMap;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.dei.perla.parser.grammar.errors.ErrorPool;
import org.dei.perla.parser.grammar.errors.IError.ErrorPriority;
import org.dei.perla.utils.logger.LogRecord.Type;
import org.dei.perla.utils.logger.LogRecord.Verbosity;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

public class DefinedTypes {

	/**
	 * Riferimento all'istanza attuale
	 */
	private static DefinedTypes userDefinedTypesInstance = null;
	
	/**
	 * Indica dove sono memorizzati i descrittori xml dei tipi definiti dall'utente
	 */ 
	private static final String USERDESCRIPTORSLOCATION = "UserTypes";
	
	/**
	 * Indica dove sono memorizzati i descrittori xml dei tipi built-in
	 */
	private static final String BUILTINDESCRTIPTORSLOCATION = "BuiltInTypes";

	// Nomi dei tags del file
	private static final String BUILTINCONSTANT= "BuiltInConstant";
	private static final String USERDEFINEDCONSTANT = "UserDefinedConstant";
	private static final String ENABLED = "enabled";
	private static final String PACKAGE = "package";
	private static final String ALIAS = "alias";
	private static final String CLASS = "class";

	/**
	 *  Struttura di memorizzazione tipi user defined
	 */
	private static Map<String, String> userDefinedTypes = null;
	
	/** 
	 * Struttura di memorizzazione tipi built-in
	 */
	private static Map<String, String> builtInTypes = null;

	/**
	 * Controlla se e' gia' stato inserito un tipo user defined con lo stesso alias
	 * 
	 * @param alias Nome dell'alias della costante
	 * @return true se nella struttura dati esiste gia' la chiave data
	 */
	public boolean containsUserDefinedKey(String alias) {

		return userDefinedTypes.containsKey(alias.toUpperCase());
	}
	
	/**
	 * Controlla se e' gia' stato inserito un tipo built in con lo stesso alias
	 * 
	 * @param alias Nome dell'alias della costante
	 * @return true se nella struttura dati esiste gia' la chiave data
	 */
	public boolean containsBuiltInKey(String alias) {
		
		return builtInTypes.containsKey(alias.toUpperCase());
	}

	/**
	 * Recupera la stringa della costante per istanzarla tramite reflection
	 * 
	 * @param alias
	 *            Nome dell'alias della costante
	 * @return Il percorso del package della costante
	 */
	public String getUserDefinedType(String alias) {
		
		return userDefinedTypes.get(alias);
	}

	/**
	 * Costruttore, si occupa del caricamento dei tipi
	 */
	private DefinedTypes() {

		// Creazione delle strutture dati
		builtInTypes = new HashMap<String, String>();
		userDefinedTypes = new HashMap<String, String>();

		//Caricamento dei tipi built-in
		this.loadBuitlInTypes();
		
		// Caricamento dei tipi definiti dall'utente
		this.loadUserDefinedTypes();
	}
	
	/**
	 * Assicura che solo un oggetto di tipo UserDefinedTypes esista
	 * @return oggetto UserDefinedTypes
	 */
	public static DefinedTypes getInstance() {
		
		// Controlla se esiste gia' un'istanza dell'oggetto UserDefinedTypes
		if (userDefinedTypesInstance == null) {
			
			userDefinedTypesInstance = new DefinedTypes();
		}
		
		return userDefinedTypesInstance;
	}
	
	/**
	 * Libera l'istanza in caso di distruzione oggetto
	 */
	public void finalize() {
		
		userDefinedTypesInstance = null;
	}

	/**
	 * Aggiunge un tipo definito dall'utente
	 * 
	 * @param packageName nome del package
	 * @param className nome della classe
	 * @param alias alias assegnato al tipo
	 */
	private void addUserDefinedType(String packageName, String className,
			String alias) {

		userDefinedTypes.put(alias, packageName + "." + className);
	}
	
	/**
	 * Aggiunge un tipo built in
	 * 
	 * @param packageName nome del package
	 * @param className nome della classe
	 * @param alias alias assegnato al tipo
	 */
	private void addBuiltInType(String packageName, String className, String alias) {
		
		builtInTypes.put(alias, packageName + "." + className);
	}

	/**
	 * Controlla se e' gia' stato definito un tipo user defined con stessa classe
	 * 
	 * @param packageName nome del package in cui il tipo e' definito
	 * @param className nome della classe
	 * @return true se il tipo e' gia' stato definito, false altrimenti
	 */
	private boolean containsUserDefinedType(String packageName, String className) {
		return userDefinedTypes.containsValue(packageName + "." + className);
	}
	
	/**
	 * Controlla se e' gia' stato definito un tipo built in con stessa classe
	 * 
	 * @param packageName nome del package in cui il tipo e' definito
	 * @param className nome della classe
	 * @return true se il tipo e' gia' stato definito, false altrimenti
	 */
	private boolean containsBuiltInType(String packageName, String className) {
		
		return builtInTypes.containsValue(packageName + "." + className);
	}
	
	public Class<?> getTypeClass(String typeName) {
		try {
			if (DefinedTypes.getInstance().containsBuiltInKey(typeName))
				return Class.forName(builtInTypes.get(typeName));
			else if (DefinedTypes.getInstance().containsUserDefinedKey(typeName))
				return Class.forName(userDefinedTypes.get(typeName));
			return null;
		} catch (ClassNotFoundException e) {
			//TODO Gestire questa eccezione.
			return null;
		}
	}
	
	
	/**
	 * Caricamento tipi built-in
	 */
	private void loadBuitlInTypes() {

		ParserLogger.getInstance().printMessage(Type.Information, Verbosity.High, "DefinedType.loadBuiltInTypes", "Caricamento descrittori tipi BuiltIn", "");

		// Estrazione files xml
		File descriptorsLocation = new File(BUILTINDESCRTIPTORSLOCATION);
		String[] xmlDescriptors = descriptorsLocation.list(new XMLFilenameFilter());
		System.out.println(BUILTINDESCRTIPTORSLOCATION + File.separatorChar + xmlDescriptors[0]);
		// Analisi files
		for (String xmlFilename : xmlDescriptors) {

			ParserLogger.getInstance().printMessage(Type.Information, Verbosity.High, "DefinedType.loadBuiltInTypes", "Analisi del file: " + xmlFilename, "");

			// Variabili temporanee per il parsing
			NodeList tempList;
			Element tempNode;
			String packageName;
			String className;
			String alias;
			
			File xmlFile = new File(BUILTINDESCRTIPTORSLOCATION + File.separatorChar + xmlFilename);

			try {

				// Carica il documento
				DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
				Document doc = builder.parse(xmlFile);

				// Ottieni le informazioni per ogni tipo di dato definito nel
				// documento
				NodeList nodeList = doc.getElementsByTagName(BUILTINCONSTANT);
				for (int i = 0; i < nodeList.getLength(); i++) {

					Element e = (Element) nodeList.item(i);

					// Esegue il parsing solo se l'utente non ha disabilitato il
					// tipo
					if (Boolean.parseBoolean(e.getAttribute(ENABLED)) == true) {

						// Nome del package
						tempList = e.getElementsByTagName(PACKAGE);
						tempNode = (Element) tempList.item(0);
						packageName = tempNode.getTextContent();

						// Nome della classe
						tempList = e.getElementsByTagName(CLASS);
						tempNode = (Element) tempList.item(0);
						className = tempNode.getTextContent();

						// Alias del tipo
						tempList = e.getElementsByTagName(ALIAS);
						tempNode = (Element) tempList.item(0);
						// I tipi sono case insensitive
						alias = tempNode.getTextContent().toUpperCase();
						
						// Aggiungi il tipo nella Map
						if (containsBuiltInKey(alias)) {

							// Errore, sono stati dichiarati piu' tipi Built-In con lo stesso alias
							ParserLogger.getInstance().printMessage(Type.Error, Verbosity.Low, "DefinedTypes.loadBuiltInTypes()", 
									"Due tipi Built-In sono stati dichiarati con lo stesso alias", "Alias: " + alias);
							ErrorPool.getInstance().addError("Due tipi Built-In sono stati dichiarati con lo stesso alias", "Alias: " + alias, ErrorPriority.HIGH);

						} else if (this.containsBuiltInType(packageName, className)) {

							// Errore, due tipi con alias diversi
							// dichiarati con stessa classe
							ParserLogger.getInstance().printMessage(Type.Error, Verbosity.Low, "DefinedTypes.loadBuiltInTypes()", "Due tipi Built-In con alias diversi sono stati dichiarati con la stessa classe", "Alias: " + alias + ", Classe: " + packageName + "." + className);
							ErrorPool.getInstance().addError("Due tipi Built-In con alias diversi sono stati dichiarati con la stessa classe", 
									"Alias: " + alias + ", Classe: " + packageName + "." + className, ErrorPriority.HIGH);

						} else {

							// Nessun problema, si puo' procedere ad inserire il tipo
							addBuiltInType(packageName, className, alias);

							// Visualizza informazioni a video
							ParserLogger.getInstance().printMessage(Type.Information, Verbosity.High, "DefinedType.loadBuiltInTypes", "INSERIMENTO Alias: " + alias + ", classe: " + packageName + "." + className, "");

						}
					}
				}

			} catch (Exception e) {
				// Cosa diciamo all'utente se qualcosa va storto?
				ParserLogger.getInstance().printMessage(Type.Error, Verbosity.Low, "DefinedTypes.loadBuiltInTypes()", "Errore sconosciuto durante il caricamento dei descrittori per i tipi Built-In", "Nessuna informazione aggiuntiva disponibile");
				ErrorPool.getInstance().addError("Errore sconosciuto durante il caricamento dei descrittori per i tipi Built-In", "", ErrorPriority.HIGH);
			}
		}

		// Stampa riepilogo
		ParserLogger.getInstance().printMessage(Type.Information, Verbosity.High, "DefinedType.loadBuiltInTypes", "Fine caricamento, numero tipi inseriti: " + builtInTypes.size(), "");
	}

	/**
	 * Carica i descrittori xml dei tipi definiti dall'utente
	 */
	private void loadUserDefinedTypes() {

		ParserLogger.getInstance().printMessage(Type.Information, Verbosity.High, "DefinedType.loadUserDefinedTypes", "Caricamento descrittori tipi UserDefined", "");

		// Estrazione files xml
		File descriptorsLocation = new File(USERDESCRIPTORSLOCATION);
		String[] xmlDescriptors = descriptorsLocation
				.list(new XMLFilenameFilter());

		// Analisi files
		for (String xmlFilename : xmlDescriptors) {

			ParserLogger.getInstance().printMessage(Type.Information, Verbosity.High, "DefinedType.loadUserDefinedTypes", "Analisi del file: " + xmlFilename, "");

			// Variabili temporanee per il parsing
			NodeList tempList;
			Element tempNode;
			String packageName;
			String className;
			String alias;

			File xmlFile = new File(USERDESCRIPTORSLOCATION + File.separatorChar
					+ xmlFilename);

			try {

				// Carica il documento
				DocumentBuilder builder = DocumentBuilderFactory.newInstance()
						.newDocumentBuilder();
				Document doc = builder.parse(xmlFile);

				// Ottieni le informazioni per ogni tipo di dato definito nel
				// documento
				NodeList nodeList = doc
						.getElementsByTagName(USERDEFINEDCONSTANT);
				for (int i = 0; i < nodeList.getLength(); i++) {

					Element e = (Element) nodeList.item(i);

					// Esegue il parsing solo se l'utente non ha disabilitato il
					// tipo
					if (Boolean.parseBoolean(e.getAttribute(ENABLED)) == true) {

						// Nome del package
						tempList = e.getElementsByTagName(PACKAGE);
						tempNode = (Element) tempList.item(0);
						packageName = tempNode.getTextContent();

						// Nome della classe
						tempList = e.getElementsByTagName(CLASS);
						tempNode = (Element) tempList.item(0);
						className = tempNode.getTextContent();

						// Alias del tipo
						tempList = e.getElementsByTagName(ALIAS);
						tempNode = (Element) tempList.item(0);
						// I tipi sono case insensitive
						alias = tempNode.getTextContent().toUpperCase();
						
						// Aggiungi il tipo nella Map
						if (containsBuiltInKey(alias)) {
							
							// Errore, sono stati dichiarati piu' tipi built in con lo stesso alias
							ParserLogger.getInstance().printMessage(Type.Error, Verbosity.Low, "DefinedTypes.loadUserDefinedTypes()", 
									"Un tipo User Defined e' stato dichiarato con lo stesso nome di un tipo Built-in", "Alias: " + alias);
							ErrorPool.getInstance().addError("Un tipo User Defined e' stato dichiarato con lo stesso nome di un tipo Built-in", "Alias: " + alias, ErrorPriority.HIGH);
							
						} else if (containsUserDefinedKey(alias)) {

							// Errore, sono stati dichiarati piu' tipi user defined con lo stesso alias
							ParserLogger.getInstance().printMessage(Type.Error, Verbosity.Low, "DefinedTypes.loadUserDefinedTypes()", 
									"Due tipi User Defined sono stati dichiarati con lo stesso alias", "Alias: " + alias);
							ErrorPool.getInstance().addError("Due tipi User Defined sono stati dichiarati con lo stesso alias", "Alias: " + alias, ErrorPriority.HIGH);

						} else if (this.containsBuiltInType(packageName, className)) {
							
							// Errore, due tipi built in con alias diversi dichiarati con stessa classe
							ParserLogger.getInstance().printMessage(Type.Error, Verbosity.Low, "DefinedTypes.loadUserDefinedTypes()", "Un tipo User Defined e' stato dichiarato con la stessa classe di un tipo Built-In", "Alias: " + alias + ", Classe: " + packageName + "." + className);
							ErrorPool.getInstance().addError("Un tipo User Defined e' stato dichiarato con la stessa classe di un tipo Built-In", 
									"Alias: " + alias + ", Classe: " + packageName + "." + className, ErrorPriority.HIGH);
							
						} else if (this.containsUserDefinedType(packageName, className)) {

							// Errore, due tipi user defined con alias diversi dichiarati con stessa classe
							ParserLogger.getInstance().printMessage(Type.Error, Verbosity.Low, "DefinedTypes.loadUserDefinedTypes()", "Due tipi User Defined con alias diversi sono stati dichiarati con la stessa classe", "Alias: " + alias + ", Classe: " + packageName + "." + className);
							ErrorPool.getInstance().addError("Due tipi User Defined con alias diversi sono stati dichiarati con la stessa classe", 
									"Alias: " + alias + ", Classe: " + packageName + "." + className, ErrorPriority.HIGH);

						} else {

							// Nessun problema, si puo' procedere ad inserire il
							// tipo
							addUserDefinedType(packageName, className, alias);

							// Visualizza informazioni a video
							ParserLogger.getInstance().printMessage(Type.Information, Verbosity.High, "DefinedType.loadUserDefinedTypes", "INSERIMENTO Alias: " + alias + ", classe: " + packageName + "." + className, "");

						}
					}
				}

			} catch (Exception e) {

				// Cosa diciamo all'utente se qualcosa va storto?
				ParserLogger.getInstance().printMessage(Type.Error, Verbosity.Low, "DefinedTypes.loadUserDefinedTypes()", "Errore sconosciuto durante il caricamento dei descrittori per i tipi User Defined", "Nessuna informazione aggiuntiva disponibile");
				ErrorPool.getInstance().addError("Errore sconosciuto durante il caricamento dei descrittori per i tipi User Defined", "", ErrorPriority.HIGH);
			}
		}

		// Stampa riepilogo
		ParserLogger.getInstance().printMessage(Type.Information, Verbosity.High, "DefinedType.loadUserDefinedTypes", "Fine caricamento, numero tipi inseriti: " + userDefinedTypes.size(), "");
	}
}
